/*
 * Copyright (c) 2010, Brookhaven National Laboratory
 * All rights reserved.
 * The software in this package is published under the terms
 * of the BSD license a copy of which has been included with
 * this distribution in the license.txt file.
 */


package org.oxtypes.protobuf.impl;

import java.io.IOException;

import com.google.protobuf.Descriptors.*;
import com.google.protobuf.WireFormat.*;
import com.google.protobuf.CodedOutputStream;

import org.oxtypes.DynamicData;

// OxTypes

import com.google.protobuf.PublicWireFormat;

/**
 *
 */
public class PackedFieldWriter {

    public PackedFieldWriter(){
    }

    public void computeByteSize(DynamicData dd, FieldDescriptor fd,
            LengthDelimitedField ldField){

        int id = fd.getNumber();
        
        DynamicData field = dd.getComplexValue(id);
        int length = field.getLength();
        if(length == 0) return;

        // tag size
        ldField.addSize(CodedOutputStream.computeTagSize(id));

        switch(fd.getLiteType()){
            // fixed size types
            case DOUBLE  : computeDoublesSize(length, ldField); break;
            case FLOAT   : computeFloatsSize(length, ldField);  break;
            case FIXED64 : computeFixed64sSize(length, ldField); break;
            case FIXED32 : computeFixed32sSize(length, ldField); break;
            case SFIXED64: computeSFixed64sSize(length, ldField); break;
            case SFIXED32: computeSFixed32sSize(length, ldField); break;
            // variable size types
            case INT64   : computeInt64sSize(field, id, ldField); break;
            case INT32   : computeInt32sSize(field, id, ldField); break;
            case UINT64  : computeUInt64sSize(field, id, ldField); break;
            case UINT32  : computeUInt32sSize(field, id, ldField); break;
            case SINT64  : computeSInt64sSize(field, id, ldField); break;
            case SINT32  : computeSInt32sSize(field, id, ldField); break;
            case BOOL    : computeBoolsSize(field, id, ldField); break;
            case ENUM    : computeEnumsSize(field, id, ldField); break;
            default:
              throw new IllegalArgumentException("type of the packed field is not primitive");

        }
    }

    public void write(DynamicData dd,  FieldDescriptor fd,
             LengthDelimitedField ldField,
             CodedOutputStream output) throws IOException {

        int id = fd.getNumber();
        DynamicData field = dd.getComplexValue(id);

        int length = field.getLength();
        if(length == 0) return;

        // write tag
        output.writeTag(id, PublicWireFormat.WIRETYPE_LENGTH_DELIMITED);

        switch(fd.getLiteType()){
            // fixed size types
            case DOUBLE  : writeDoublesNoTag(field, output); break;
            case FLOAT   : writeFloatsNoTag(field, output);  break;
            case FIXED64 : writeFixed64sNoTag(field, output); break;
            case FIXED32 : writeFixed32sNoTag(field, output); break;
            case SFIXED64: writeSFixed64sNoTag(field, output); break;
            case SFIXED32: writeSFixed32sNoTag(field, output); break;
            // variable size types
            case INT64   : writeInt64sNoTag(field, ldField.getField(id), output); break;
            case INT32   : writeInt32sNoTag(field, ldField.getField(id), output); break;
            case UINT64  : writeUInt64sNoTag(field, ldField.getField(id), output); break;
            case UINT32  : writeUInt32sNoTag(field, ldField.getField(id), output); break;
            case SINT64  : writeSInt64sNoTag(field, ldField.getField(id), output); break;
            case SINT32  : writeSInt32sNoTag(field, ldField.getField(id), output); break;
            case BOOL    : writeBoolsNoTag(field, ldField.getField(id), output); break;
            case ENUM    : writeEnumsNoTag(field, ldField.getField(id), output); break;
            default:
              throw new IllegalArgumentException("type of the packed field is not primitive");
        }
    }

    private void computeDoublesSize(int length,
            LengthDelimitedField ldField){
        ldField.addSize(CodedOutputStream.computeRawVarint32Size(length));
        ldField.addSize(length*doubleSize);
    }
    
    private void writeDoublesNoTag(DynamicData dd, 
            CodedOutputStream output) throws IOException {
        
        int length = dd.getLength();

        // write size
        output.writeRawVarint32(length*doubleSize);

        // write data
        for (int i=0; i < length; i++) {
          output.writeDoubleNoTag(dd.getFloat64Value(i));
        }
    }

    private void computeFloatsSize(int length,
            LengthDelimitedField ldField){
        ldField.addSize(CodedOutputStream.computeRawVarint32Size(length));
        ldField.addSize(length*floatSize);
    }

    private void writeFloatsNoTag(DynamicData dd,
            CodedOutputStream output) throws IOException {

        int length = dd.getLength();

        // write size
        output.writeRawVarint32(length*floatSize);

        // write data
        for (int i=0; i < length; i++) {
          output.writeFloatNoTag(dd.getFloat32Value(i));
        }
    }

    private void computeFixed64sSize(int length,
            LengthDelimitedField ldField){
        ldField.addSize(CodedOutputStream.computeRawVarint32Size(length));
        ldField.addSize(length*fixed64Size);
    }

    private void writeFixed64sNoTag(DynamicData dd,
            CodedOutputStream output) throws IOException {

        int length = dd.getLength();

        // write size
        output.writeRawVarint32(length*fixed64Size);

        // write data
        for (int i=0; i < length; i++) {
          output.writeFixed64NoTag(dd.getInt64Value(i));
        }
    }

    private void computeFixed32sSize(int length,
            LengthDelimitedField ldField){
        ldField.addSize(CodedOutputStream.computeRawVarint32Size(length));
        ldField.addSize(length*fixed32Size);
    }

    private void writeFixed32sNoTag(DynamicData dd,
            CodedOutputStream output) throws IOException {

        int length = dd.getLength();

        // write size
        output.writeRawVarint32(length*fixed32Size);

        // write data
        for (int i=0; i < length; i++) {
          output.writeFixed32NoTag(dd.getInt32Value(i));
        }
    }

    private void computeSFixed64sSize(int length,
            LengthDelimitedField ldField){
        ldField.addSize(CodedOutputStream.computeRawVarint32Size(length));
        ldField.addSize(length*sfixed64Size);
    }

    private void writeSFixed64sNoTag(DynamicData dd,
            CodedOutputStream output) throws IOException {

        int length = dd.getLength();

        // write size
        output.writeRawVarint32(length*sfixed64Size);

        // write data
        for (int i=0; i < length; i++) {
          output.writeSFixed64NoTag(dd.getInt64Value(i));
        }
    }

    private void computeSFixed32sSize(int length,
            LengthDelimitedField ldField){
        ldField.addSize(CodedOutputStream.computeRawVarint32Size(length));
        ldField.addSize(length*sfixed32Size);
    }

    private void writeSFixed32sNoTag(DynamicData dd,
            CodedOutputStream output) throws IOException {

        int length = dd.getLength();

        // write size
        output.writeRawVarint32(length*sfixed32Size);

        // write data
        for (int i=0; i < length; i++) {
          output.writeSFixed32NoTag(dd.getInt32Value(i));
        }
    }

    private void computeInt64sSize(DynamicData dd, int id,
            LengthDelimitedField compositeField){

        int length = dd.getLength();
        int byteSize = 0;

        // elements
        for (int i=0; i < length; i++) {
          byteSize += CodedOutputStream.computeInt64SizeNoTag(dd.getInt64Value(i));
        }

        // size
        byteSize += CodedOutputStream.computeRawVarint32Size(byteSize);

        LengthDelimitedField ldField = new PackedField();
        ldField.addSize(byteSize);
        compositeField.addField(id, ldField);

    }

    private void writeInt64sNoTag(DynamicData dd, LengthDelimitedField ldField,
            CodedOutputStream output) throws IOException {

        int length = dd.getLength();

        // write size
        output.writeRawVarint32(ldField.getSize());

        // write data
        for (int i=0; i < length; i++) {
          output.writeInt64NoTag(dd.getInt64Value(i));
        }
    }

    private void computeInt32sSize(DynamicData dd, int id,
            LengthDelimitedField compositeField){

        int length = dd.getLength();
        int byteSize = 0;

        // elements
        for (int i=0; i < length; i++) {
          byteSize += CodedOutputStream.computeInt32SizeNoTag(dd.getInt32Value(i));
        }

        // size
        byteSize += CodedOutputStream.computeRawVarint32Size(byteSize);

        LengthDelimitedField ldField = new PackedField();
        ldField.addSize(byteSize);
        compositeField.addField(id, ldField);

    }

    private void writeInt32sNoTag(DynamicData dd, LengthDelimitedField ldField,
            CodedOutputStream output) throws IOException {

        int length = dd.getLength();

        // write size
        output.writeRawVarint32(ldField.getSize());

        // write data
        for (int i=0; i < length; i++) {
          output.writeInt32NoTag(dd.getInt32Value(i));
        }
    }

    private void computeUInt64sSize(DynamicData dd, int id,
            LengthDelimitedField compositeField){

        int length = dd.getLength();
        int byteSize = 0;

        // elements
        for (int i=0; i < length; i++) {
          byteSize += CodedOutputStream.computeUInt64SizeNoTag(dd.getInt64Value(i));
        }

        // size
        byteSize += CodedOutputStream.computeRawVarint32Size(byteSize);

        LengthDelimitedField ldField = new PackedField();
        ldField.addSize(byteSize);
        compositeField.addField(id, ldField);

    }

    private void writeUInt64sNoTag(DynamicData dd, LengthDelimitedField ldField,
            CodedOutputStream output) throws IOException {

        int length = dd.getLength();

        // write size
        output.writeRawVarint32(ldField.getSize());

        // write data
        for (int i=0; i < length; i++) {
          output.writeUInt64NoTag(dd.getInt64Value(i));
        }
    }

    private void computeUInt32sSize(DynamicData dd, int id,
            LengthDelimitedField compositeField){

        int length = dd.getLength();
        int byteSize = 0;

        // elements
        for (int i=0; i < length; i++) {
          byteSize += CodedOutputStream.computeUInt32SizeNoTag(dd.getInt32Value(i));
        }

        // size
        byteSize += CodedOutputStream.computeRawVarint32Size(byteSize);

        LengthDelimitedField ldField = new PackedField();
        ldField.addSize(byteSize);
        compositeField.addField(id, ldField);

    }

    private void writeUInt32sNoTag(DynamicData dd, LengthDelimitedField ldField,
            CodedOutputStream output) throws IOException {

        int length = dd.getLength();

        // write size
        output.writeRawVarint32(ldField.getSize());

        // write data
        for (int i=0; i < length; i++) {
          output.writeUInt32NoTag(dd.getInt32Value(i));
        }
    }

    private void computeSInt64sSize(DynamicData dd, int id,
            LengthDelimitedField compositeField){

        int length = dd.getLength();
        int byteSize = 0;

        // elements
        for (int i=0; i < length; i++) {
          byteSize += CodedOutputStream.computeSInt64SizeNoTag(dd.getInt64Value(i));
        }

        // size
        byteSize += CodedOutputStream.computeRawVarint32Size(byteSize);

        LengthDelimitedField ldField = new PackedField();
        ldField.addSize(byteSize);
        compositeField.addField(id, ldField);

    }

    private void writeSInt64sNoTag(DynamicData dd, LengthDelimitedField ldField,
            CodedOutputStream output) throws IOException {

        int length = dd.getLength();

        // write size
        output.writeRawVarint32(ldField.getSize());

        // write data
        for (int i=0; i < length; i++) {
          output.writeSInt64NoTag(dd.getInt64Value(i));
        }
    }

    private void computeSInt32sSize(DynamicData dd, int id,
            LengthDelimitedField compositeField){

        int length = dd.getLength();
        int byteSize = 0;

        // elements
        for (int i=0; i < length; i++) {
          byteSize += CodedOutputStream.computeSInt32SizeNoTag(dd.getInt32Value(i));
        }

        // size
        byteSize += CodedOutputStream.computeRawVarint32Size(byteSize);

        LengthDelimitedField ldField = new PackedField();
        ldField.addSize(byteSize);
        compositeField.addField(id, ldField);

    }

    private void writeSInt32sNoTag(DynamicData dd, LengthDelimitedField ldField,
            CodedOutputStream output) throws IOException {

        int length = dd.getLength();

        // write size
        output.writeRawVarint32(ldField.getSize());

        // write data
        for (int i=0; i < length; i++) {
          output.writeSInt32NoTag(dd.getInt32Value(i));
        }
    }

    private void computeBoolsSize(DynamicData dd, int id,
            LengthDelimitedField compositeField){

        int length = dd.getLength();
        int byteSize = 0;

        // elements
        for (int i=0; i < length; i++) {
          byteSize += CodedOutputStream.computeBoolSizeNoTag(dd.getBooleanValue(i));
        }

        // size
        byteSize += CodedOutputStream.computeRawVarint32Size(byteSize);

        LengthDelimitedField ldField = new PackedField();
        ldField.addSize(byteSize);
        compositeField.addField(id, ldField);

    }

    private void writeBoolsNoTag(DynamicData dd, LengthDelimitedField ldField,
            CodedOutputStream output) throws IOException {

        int length = dd.getLength();

        // write size
        output.writeRawVarint32(ldField.getSize());

        // write data
        for (int i=0; i < length; i++) {
          output.writeBoolNoTag(dd.getBooleanValue(i));
        }
    }

   private void computeEnumsSize(DynamicData dd, int id,
            LengthDelimitedField compositeField){

        int length = dd.getLength();
        int byteSize = 0;

        // elements
        for (int i=0; i < length; i++) {
          byteSize += CodedOutputStream.computeEnumSizeNoTag(dd.getInt32Value(i));
        }

        // size
        byteSize += CodedOutputStream.computeRawVarint32Size(byteSize);

        LengthDelimitedField ldField = new PackedField();
        ldField.addSize(byteSize);
        compositeField.addField(id, ldField);

    }

    private void writeEnumsNoTag(DynamicData dd, LengthDelimitedField ldField,
            CodedOutputStream output) throws IOException {

        int length = dd.getLength();

        // write size
        output.writeRawVarint32(ldField.getSize());

        // write data
        for (int i=0; i < length; i++) {
          output.writeEnumNoTag(dd.getInt32Value(i));
        }
    }

    public class PackedField implements LengthDelimitedField {

        private int byteSize;

        public PackedField() {
        }

        public int getSize(){
            return byteSize;
        }

        public void addSize(int size){
            byteSize += size;
        }

        public void addField(int id, LengthDelimitedField ldField){
            throw new java.lang.UnsupportedOperationException();
        }

        public LengthDelimitedField getField(int id){
            throw new java.lang.UnsupportedOperationException();
        }
    }

    static private int doubleSize = CodedOutputStream.computeDoubleSizeNoTag(0.0);
    static private int floatSize = CodedOutputStream.computeFloatSizeNoTag(0);
    static private int fixed64Size = CodedOutputStream.computeFixed64SizeNoTag(0);
    static private int fixed32Size = CodedOutputStream.computeFixed32SizeNoTag(0);
    static private int sfixed64Size = CodedOutputStream.computeSFixed64SizeNoTag(0);
    static private int sfixed32Size = CodedOutputStream.computeSFixed32SizeNoTag(0);

}
